﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ProLinq.Projection.Configuration;
using ProLinq.Projection.Visitors.Optimization;
using ProLinq.Toolkit;
using ProLinq.Tracing;

namespace ProLinq.Projection.Visitors
{
	/// <summary>
	/// Projects an expression.
	/// Uses inverted direction of projections.
	/// </summary>
	internal class ProjectionVisitor : ExpressionVisitor
	{
		#region Nested

		/// <summary>
		///     Exception used to bypass call stack and to return a valid inner expression
		/// </summary>
		private class SliceException : Exception
		{
			public SliceException(Expression originalExpression, Expression newExpression, Exception innerException)
				: base(innerException.Message, innerException)
			{
				this.OriginalExpression = originalExpression;
				this.NewExpression = newExpression;
			}

			public Expression OriginalExpression { get; private set; }
			public Expression NewExpression { get; private set; }
		}

		#endregion

		#region Fields

		private readonly Stack<IEnumerable<ParameterExpression>> _lambdaArgsStack =
			new Stack<IEnumerable<ParameterExpression>>();

		private readonly Dictionary<ParameterExpression, ParameterExpression> _params =
			new Dictionary<ParameterExpression, ParameterExpression>();

		private Boolean _isFirstVisit = true;

		#endregion

		#region Constructors

		public ProjectionVisitor()
		{
			this.Projections = new List<Configuration.Projection>();
			this.EnableOptimization = true;
			this.FullProjectionRequired = false;
		}

		#endregion

		#region Properties

		/// <summary>
		/// True to throw an exception if the expression cannot be fully projected
		/// </summary>
		public Boolean FullProjectionRequired { get; set; }

		/// <summary>
		/// True to enable optimization in case of partial processing.
		/// </summary>
		public Boolean EnableOptimization { get; set; }

		/// <summary>
		/// True if all anonymous types should be replaced automatically. 
		/// Otherwise only those participating in other projections should be replaced.
		/// </summary>
		public Boolean ProjectAllAnonymousTypes { get; set; }

		/// <summary>
		///     List of projections to be used for pre processing and populated during it with projections of anonymous types.
		/// </summary>
		public List<Configuration.Projection> Projections { get; private set; }

		/// <summary>
		///     Expresssion that should be used for post processing.
		/// </summary>
		public Expression ProcessedExpressionPart { get; private set; }

		#endregion

		#region Methods

		/// <summary>
		///     Returns projected type or the same type
		/// </summary>
		/// <param name="sourceType"></param>
		/// <returns></returns>
		private Type ProjectType(Type resultType)
		{
			// if type is generic (not anonymous though - another logic for them) then replace it
			if (resultType.IsGenericType && !resultType.IsAnonymousType())
			{
				var projectedArgs = resultType.GetGenericArguments().Select(a => this.ProjectType(a)).ToArray();
				return resultType.GetGenericTypeDefinition().MakeGenericType(projectedArgs);
			}

			// search for configured projection
			return this.Projections.Where(p => p.DestinationType == resultType).Select(p => p.SourceType).FirstOrDefault() ??
				   resultType;
		}

		private Configuration.Projection FindProjection(Type resultType)
		{
			return this.Projections.FirstOrDefault(p => p.DestinationType == resultType);
		}

		private void AddProjection(Configuration.Projection projection)
		{
			int index = this.Projections.FindIndex(p => p.DestinationType == projection.DestinationType);
			if (index >= 0)
			{
				this.Projections[index] = projection;
			}
			else
			{
				this.Projections.Add(projection);
			}
		}

		private Type GetAnonymousType(Type[] args)
		{
			// TODO: use Reflection.Emit to generate anonymous type for any arguments count
			if (args.Length > 8)
				throw new NotSupportedException(
					"Projection extension doesn't support selecting types with more than 8 members, except main projected type");

			return Type.GetType("ProLinq.Projection.MutableTuple`" + args.Length).MakeGenericType(args);
		}

		private Type GetMemberType(MemberInfo member)
		{
			if (member is FieldInfo)
				return ((FieldInfo) member).FieldType;
			if (member is PropertyInfo)
				return ((PropertyInfo) member).PropertyType;
			return null;
		}

		#endregion

		#region Overrides

		public override Expression Visit(Expression node)
		{
			// separate logic for first external call and later inner calls
			if (this._isFirstVisit)
			{
				this._isFirstVisit = false;
				try
				{
					this.ProcessedExpressionPart = node;

					// returns here when expression is projected fully
					return base.Visit(node);
				}
				catch (SliceException exc)
				{
					if (this.FullProjectionRequired)
					{
						throw new ProjectionException("The expression cannot be fully projected.", exc.InnerException);
					}

					// preprocessing expression
					this.ProcessedExpressionPart = exc.OriginalExpression;

					TraceSources.Projection.TraceEvent(TraceEventType.Information, 0,
					                                   "Expression will be processed partially due to an error: {0}",
					                                   exc.Message);
					TraceSources.Projection.TraceEvent(TraceEventType.Verbose, 0, exc.InnerException.ToString());

					if (this.EnableOptimization)
					{
						// trying to optimize
						try
						{
							var optimizator = new OptimizationVisitor
								{
									OriginalExpression = node,
									OriginalExpressionProcessedPart = this.ProcessedExpressionPart,
									Projections = this.Projections
								};

							Expression optimizedExpression = optimizator.Visit(exc.NewExpression);
							if (optimizedExpression != exc.NewExpression)
							{
								TraceSources.Projection.TraceEvent(TraceEventType.Information, 0, "Expression for source is optimized.");
								return optimizedExpression;
							}
						}
						catch (Exception exc1)
						{
							TraceSources.Projection.TraceEvent(TraceEventType.Information, 0,
							                                   "Expression cannot be optimized due to an error: {0}", exc1.Message);
							TraceSources.Projection.TraceEvent(TraceEventType.Verbose, 0, exc1.ToString());
						}
					}

					// returns here when expression is projected partially
					return exc.NewExpression;
				}
			}
			return base.Visit(node);
		}

		protected override Expression VisitMethodCall(MethodCallExpression node)
		{
			MethodInfo methodInfo = node.Method;

			// Queryable and Enumerable extension methods
			if (methodInfo.IsGenericMethod)
			{
				var args = new Expression[node.Arguments.Count];

				if (methodInfo.DeclaringType == typeof (Queryable))
				{
					// queryable extension methods
					// main call chain
					// passes exceptions through
					args[0] = this.Visit(node.Arguments[0]);

					// arguments call chain
					try
					{
						for (Int32 i = 1; i < args.Length; i++)
						{
							args[i] = this.Visit(node.Arguments[i]);
						}
					}
					catch (Exception exc)
					{
						throw new SliceException(node.Arguments[0], args[0], exc);
					}
				}
				else
				{
					// all other generic methods
					for (Int32 i = 0; i < args.Length; i++)
					{
						args[i] = this.Visit(node.Arguments[i]);
					}
				}

				// convert types
				Type[] genericArgs = methodInfo.GetGenericArguments().Select(this.ProjectType).ToArray();
				methodInfo = methodInfo.GetGenericMethodDefinition();
				methodInfo = methodInfo.MakeGenericMethod(genericArgs);

				return Expression.Call(null, methodInfo, args);;
			}
			return base.VisitMethodCall(node);
		}

		protected override Expression VisitConstant(ConstantExpression c)
		{
			// Replace source
			var value = c.Value as IQueryable;
			if (value != null)
			{
				var wrapperProvider = value.Provider as IWrapperProvider;
				if (wrapperProvider != null)
				{
					return wrapperProvider.Source.Expression;
				}
			}

			// null type replace
			if (c.Value == null)
			{
				var projectedType = this.ProjectType(c.Type);
				return Expression.Constant(c.Value, projectedType);
			}

			return base.VisitConstant(c);
		}

		protected override Expression VisitConditional(ConditionalExpression node)
		{
			var test = Visit(node.Test);
			var ifTrue = Visit(node.IfTrue);
			var ifFAlse = Visit(node.IfFalse);
			return Expression.Condition(test, ifTrue, ifFAlse); ;
		}

		protected override Expression VisitParameter(ParameterExpression node)
		{
			Configuration.Projection projection = this.FindProjection(node.Type);
			if (projection != null)
			{
				if (this._params.ContainsKey(node))
				{
					return this._params[node];
				}
				else
				{
					ParameterExpression newNode = Expression.Parameter(projection.SourceType, node.Name);
					this._params.Add(node, newNode);
					return newNode;
				}
			}
			return base.VisitParameter(node);
		}

		protected override MemberBinding VisitMemberBinding(MemberBinding node)
		{
			if (node.BindingType == MemberBindingType.Assignment)
			{
				var memberAss = (MemberAssignment) node;

				Configuration.Projection projection = this.FindProjection(node.Member.DeclaringType);
				if (projection != null)
				{
					PropertyMap propMap = projection.Map.Where(p => p.DestinationProperty == node.Member).FirstOrDefault();
					if (propMap != null)
					{
						MemberAssignment newAss = Expression.Bind(propMap.SourceProperty, this.Visit(memberAss.Expression));
						return newAss;
					}
				}
			}
			return base.VisitMemberBinding(node);
		}


		protected override Expression VisitMemberInit(MemberInitExpression node)
		{
			MemberAssignment[] originalAssignmentBindings = node.Bindings.OfType<MemberAssignment>().ToArray();
			Type[] originalArgs = originalAssignmentBindings.Select(a => this.GetMemberType(a.Member)).ToArray();

			// transform arguments
			Type[] args = originalArgs.Select(a => this.ProjectType(a)).ToArray();

			if (!this.ProjectAllAnonymousTypes || !node.Type.IsAnonymousType())
			{
				// skip processing if arguments are not modified
				if (originalArgs.SequenceEqual(args))
				{
					return base.VisitMemberInit(node);
				}
			}

			Type tupleType = this.GetAnonymousType(args);

			var projection = new Configuration.Projection(tupleType, node.Type);
			for (Int32 i = 0; i < args.Length; i++)
			{
				MemberInfo sourceProp = originalAssignmentBindings[i].Member;
				PropertyInfo prop = tupleType.GetProperty("Item" + (i + 1));

				projection.Map.Add(new PropertyMap(prop, sourceProp));
			}
			this.AddProjection(projection);

			MemberBinding[] bindings = node.Bindings.Select(b => this.VisitMemberBinding(b)).ToArray();
			NewExpression newContructorExpr = Expression.New(tupleType.GetConstructors()[0]);
			MemberInitExpression initExpr = Expression.MemberInit(newContructorExpr, bindings);
			return initExpr;
		}

		protected override Expression VisitNew(NewExpression node)
		{
			// transform arguments
			Expression[] args = node.Arguments.Select(p => this.Visit(p)).ToArray();

			if (!this.ProjectAllAnonymousTypes || !node.Type.IsAnonymousType())
			{
				// skip processing if arguments are not modified
				if (args.Select(a => a.Type).SequenceEqual(node.Arguments.Select(a => a.Type)))
				{
					return base.VisitNew(node);
				}
			}

			if (!node.Type.IsAnonymousType())
				throw new NotSupportedException("Constructors projection for non anonymous types are not supported.");

			ParameterExpression[] pars = this._lambdaArgsStack.Peek().ToArray();

			Type tupleType = this.GetAnonymousType(args.Select(a => a.Type).ToArray());

			var projection = new Configuration.Projection(tupleType, node.Type);

			for (Int32 i = 0; i < node.Members.Count; i++)
			{
				MemberInfo sourceProp = node.Members[i];
				PropertyInfo prop = tupleType.GetProperty("Item" + (i + 1));

				projection.Map.Add(new PropertyMap(prop, sourceProp));
			}

			this.AddProjection(projection);

			NewExpression newContructorExpr = Expression.New(tupleType.GetConstructors()[0]);

			var memberBindings = new List<MemberBinding>();
			for (Int32 i = 0; i < args.Length; i++)
			{
				PropertyInfo prop = tupleType.GetProperty("Item" + (i + 1));
				memberBindings.Add(Expression.Bind(prop, args[i]));
			}

			MemberInitExpression initExpr = Expression.MemberInit(newContructorExpr, memberBindings);
			return initExpr;
		}


		protected override Expression VisitMember(MemberExpression node)
		{
			Configuration.Projection projection = this.FindProjection(node.Member.DeclaringType);
			if (projection != null)
			{
				PropertyMap propMap = projection.Map.Where(p => p.DestinationProperty == node.Member).FirstOrDefault();
				if (propMap != null)
				{
					Expression modifiedParameter = this.Visit(node.Expression);
					return Expression.MakeMemberAccess(modifiedParameter, propMap.SourceProperty);
				}
			}

			return base.VisitMember(node);
		}

		protected override Expression VisitLambda<T>(Expression<T> node)
		{
			ParameterExpression[] args = node.Parameters.Select(p => this.Visit(p)).Cast<ParameterExpression>().ToArray();
			this._lambdaArgsStack.Push(args);
			Expression body = this.Visit(node.Body);
			this._lambdaArgsStack.Pop();
			return Expression.Lambda(body, args);
		}

		#endregion
	}
}